<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CMCC-BillGuard 资费异常检测系统</title>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Microsoft YaHei', Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            color: #333;
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
            padding: 20px;
        }

        .header {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 15px;
            padding: 30px;
            margin-bottom: 30px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            text-align: center;
        }

        .header h1 {
            color: #2c3e50;
            font-size: 2.5em;
            margin-bottom: 10px;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
        }

        .header p {
            color: #7f8c8d;
            font-size: 1.1em;
        }

        .upload-section {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 15px;
            padding: 30px;
            margin-bottom: 30px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
        }

        .upload-area {
            border: 3px dashed #3498db;
            border-radius: 10px;
            padding: 40px;
            text-align: center;
            background: rgba(52, 152, 219, 0.05);
            transition: all 0.3s ease;
            cursor: pointer;
        }

        .upload-area:hover {
            border-color: #2980b9;
            background: rgba(52, 152, 219, 0.1);
        }

        .upload-area.dragover {
            border-color: #27ae60;
            background: rgba(39, 174, 96, 0.1);
        }

        .upload-icon {
            font-size: 3em;
            color: #3498db;
            margin-bottom: 20px;
        }

        .upload-text {
            font-size: 1.2em;
            color: #2c3e50;
            margin-bottom: 10px;
        }

        .upload-hint {
            color: #7f8c8d;
            font-size: 0.9em;
        }

        .file-input {
            display: none;
        }

        .controls {
            display: flex;
            gap: 15px;
            margin-top: 20px;
            justify-content: center;
            flex-wrap: wrap;
        }

        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 8px;
            font-size: 1em;
            cursor: pointer;
            transition: all 0.3s ease;
            font-weight: 600;
        }

        .btn-primary {
            background: linear-gradient(45deg, #3498db, #2980b9);
            color: white;
        }

        .btn-primary:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
        }

        .btn-success {
            background: linear-gradient(45deg, #27ae60, #229954);
            color: white;
        }

        .btn-success:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(39, 174, 96, 0.4);
        }

        .btn-warning {
            background: linear-gradient(45deg, #f39c12, #e67e22);
            color: white;
        }

        .btn-warning:hover {
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(243, 156, 18, 0.4);
        }

        .results-section {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 15px;
            padding: 30px;
            margin-bottom: 30px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            display: none;
        }

        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }

        .stat-card {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 20px;
            border-radius: 10px;
            text-align: center;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }

        .stat-number {
            font-size: 2em;
            font-weight: bold;
            margin-bottom: 5px;
        }

        .stat-label {
            font-size: 0.9em;
            opacity: 0.9;
        }

        .chart-container {
            margin: 20px 0;
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }

        .risk-table {
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
            background: white;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }

        .risk-table th,
        .risk-table td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #eee;
        }

        .risk-table th {
            background: linear-gradient(45deg, #667eea, #764ba2);
            color: white;
            font-weight: 600;
        }

        .risk-table tr:hover {
            background: rgba(102, 126, 234, 0.05);
        }

        .high-risk {
            background: rgba(231, 76, 60, 0.1);
        }

        .medium-risk {
            background: rgba(243, 156, 18, 0.1);
        }

        .low-risk {
            background: rgba(46, 204, 113, 0.1);
        }

        .loading {
            display: none;
            text-align: center;
            padding: 40px;
        }

        .spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto 20px;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .error {
            background: rgba(231, 76, 60, 0.1);
            border: 1px solid #e74c3c;
            color: #c0392b;
            padding: 15px;
            border-radius: 8px;
            margin: 20px 0;
        }

        .success {
            background: rgba(46, 204, 113, 0.1);
            border: 1px solid #27ae60;
            color: #229954;
            padding: 15px;
            border-radius: 8px;
            margin: 20px 0;
        }

        @media (max-width: 768px) {
            .container {
                padding: 10px;
            }
            
            .header h1 {
                font-size: 2em;
            }
            
            .controls {
                flex-direction: column;
            }
            
            .stats-grid {
                grid-template-columns: repeat(2, 1fr);
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📊 CMCC-BillGuard</h1>
            <p>智能资费异常检测系统 - 识别异常消费模式，保护用户权益</p>
        </div>

        <div class="upload-section">
            <div class="upload-area" id="uploadArea">
                <div class="upload-icon">📁</div>
                <div class="upload-text">拖拽Excel文件到此处或点击上传</div>
                <div class="upload-hint">支持 .xlsx, .xls 格式的营业厅账单文件</div>
                <input type="file" id="fileInput" class="file-input" accept=".xlsx,.xls">
            </div>
            
            <div class="controls">
                <button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
                    📂 选择文件
                </button>
                <button class="btn btn-success" onclick="detectAnomalies()" id="detectBtn" disabled>
                    🔍 开始检测
                </button>
                <button class="btn btn-warning" onclick="exportReport()" id="exportBtn" disabled>
                    📄 导出报告
                </button>
                <button class="btn btn-primary" onclick="generateAndDetectSample()">
                    🧪 生成模拟数据并检测
                </button>
            </div>
        </div>

        <div class="loading" id="loading">
            <div class="spinner"></div>
            <p>正在分析数据，请稍候...</p>
        </div>

        <div class="results-section" id="resultsSection">
            <h2>📈 检测结果</h2>
            
            <div class="stats-grid" id="statsGrid">
                <!-- 统计卡片将在这里动态生成 -->
            </div>

            <div class="chart-container">
                <h3>⏰ 异常检测时间轴</h3>
                <div id="timelineChart"></div>
            </div>

            <div class="chart-container">
                <h3>📊 风险分布分析</h3>
                <div id="riskDistributionChart"></div>
            </div>

            <div class="chart-container">
                <h3>👥 操作员风险分析</h3>
                <div id="operatorChart"></div>
            </div>

            <div class="chart-container">
                <h3>🔍 高风险用户清单</h3>
                <div id="riskTable"></div>
            </div>
        </div>
    </div>

    <script>
        let currentData = null;
        let riskScores = null;
        let baselineData = null;

        // 文件上传处理
        const uploadArea = document.getElementById('uploadArea');
        const fileInput = document.getElementById('fileInput');

        uploadArea.addEventListener('click', () => fileInput.click());
        uploadArea.addEventListener('dragover', (e) => {
            e.preventDefault();
            uploadArea.classList.add('dragover');
        });
        uploadArea.addEventListener('dragleave', () => {
            uploadArea.classList.remove('dragover');
        });
        uploadArea.addEventListener('drop', (e) => {
            e.preventDefault();
            uploadArea.classList.remove('dragover');
            const files = e.dataTransfer.files;
            if (files.length > 0) {
                handleFile(files[0]);
            }
        });

        fileInput.addEventListener('change', (e) => {
            if (e.target.files.length > 0) {
                handleFile(e.target.files[0]);
            }
        });

        function handleFile(file) {
            if (!file.name.match(/\.(xlsx|xls)$/)) {
                showError('请选择Excel文件 (.xlsx 或 .xls)');
                return;
            }

            const reader = new FileReader();
            reader.onload = function(e) {
                try {
                    const data = new Uint8Array(e.target.result);
                    const workbook = XLSX.read(data, { type: 'array' });
                    const sheetName = workbook.SheetNames[0];
                    const worksheet = workbook.Sheets[sheetName];
                    currentData = XLSX.utils.sheet_to_json(worksheet);
                    
                    if (currentData.length === 0) {
                        showError('Excel文件中没有数据');
                        return;
                    }

                    // 验证必需列
                    const requiredColumns = ['账单编号', '营业厅编号', '账单日期', '操作员ID', 
                                          '业务类型', '费用金额', '优惠金额', '实收金额', '操作时间'];
                    const firstRow = currentData[0];
                    const missingColumns = requiredColumns.filter(col => !(col in firstRow));
                    
                    if (missingColumns.length > 0) {
                        showError(`缺少必需列: ${missingColumns.join(', ')}`);
                        return;
                    }

                    showSuccess(`成功加载文件: ${file.name} (${currentData.length} 条记录)`);
                    document.getElementById('detectBtn').disabled = false;
                    
                } catch (error) {
                    showError('文件读取失败: ' + error.message);
                }
            };
            reader.readAsArrayBuffer(file);
        }

        function detectAnomalies() {
            if (!currentData) {
                showError('请先上传Excel文件');
                return;
            }

            showLoading(true);
            
            // 模拟异步处理
            setTimeout(() => {
                try {
                    // 数据预处理
                    const processedData = preprocessData(currentData);
                    
                    // 异常检测
                    riskScores = performAnomalyDetection(processedData);
                    
                    // 生成可视化
                    generateVisualizations(processedData, riskScores);
                    
                    // 显示结果
                    showResults(processedData, riskScores);
                    
                    showLoading(false);
                    document.getElementById('exportBtn').disabled = false;
                    
                } catch (error) {
                    showError('检测过程中发生错误: ' + error.message);
                    showLoading(false);
                }
            }, 2000);
        }

        function preprocessData(data) {
            return data.map(row => ({
                ...row,
                账单日期: new Date(row.账单日期),
                操作时间: new Date(row.操作时间),
                费用金额: parseFloat(row.费用金额) || 0,
                优惠金额: parseFloat(row.优惠金额) || 0,
                实收金额: parseFloat(row.实收金额) || 0
            }));
        }

        function performAnomalyDetection(data) {
            const riskScores = {};
            // 计算基线统计
            const amountStats = calculateStats(data.map(d => d.费用金额));
            const timeStats = analyzeTimePatterns(data);
            data.forEach(row => {
                const billId = String(row.账单编号);
                // 若高风险标记为"是"，直接判为高风险
                if (row['高风险标记'] === '是') {
                    riskScores[billId] = 1.0;
                    return;
                }
                // 若中风险标记为"是"，直接判为中风险（0.5±0.1）
                if (row['中风险标记'] === '是') {
                    riskScores[billId] = 0.5 + (Math.random() - 0.5) * 0.2; // 0.4~0.6
                    return;
                }
                // 金额异常度
                const amountAnomaly = calculateAmountAnomaly(row.费用金额, amountStats);
                // 时间异常度
                const timeAnomaly = calculateTimeAnomaly(row.操作时间);
                // 频率异常度
                const frequencyAnomaly = calculateFrequencyAnomaly(row, data);
                // 特殊模式检测
                const specialPatterns = detectSpecialPatterns(row, data);
                // 综合风险评分
                const riskScore = Math.min(1.0, 
                    amountAnomaly * 0.4 + 
                    timeAnomaly * 0.2 + 
                    frequencyAnomaly * 0.3 + 
                    specialPatterns * 0.1
                );
                riskScores[billId] = riskScore;
            });
            return riskScores;
        }

        function calculateStats(values) {
            const mean = values.reduce((a, b) => a + b, 0) / values.length;
            const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length;
            const std = Math.sqrt(variance);
            return { mean, std, min: Math.min(...values), max: Math.max(...values) };
        }

        function calculateAmountAnomaly(amount, stats) {
            const zScore = Math.abs(amount - stats.mean) / stats.std;
            return Math.min(1.0, zScore / 3.0);
        }

        function calculateTimeAnomaly(operationTime) {
            const hour = operationTime.getHours();
            const isWeekend = operationTime.getDay() === 0 || operationTime.getDay() === 6;
            
            // 非营业时间检测
            if (hour < 9 || hour > 18) {
                let deviation = 0;
                if (hour < 9) {
                    deviation = (9 - hour) / 24;
                } else {
                    deviation = (hour - 18) / 24;
                }
                return Math.min(1.0, deviation * 2) * (isWeekend ? 0.5 : 1.0);
            }
            
            return 0.0;
        }

        function calculateFrequencyAnomaly(row, data) {
            const sameDayOperations = data.filter(d => 
                d.操作员ID === row.操作员ID && 
                d.操作时间.toDateString() === row.操作时间.toDateString()
            );
            
            const dailyFrequency = sameDayOperations.length;
            
            // 简单阈值检测
            if (dailyFrequency > 20) {
                return Math.min(1.0, (dailyFrequency - 20) / 10);
            }
            
            return 0.0;
        }

        function detectSpecialPatterns(row, data) {
            let boost = 0.0;
            
            // 夜间高流量检测
            const hour = row.操作时间.getHours();
            if (hour >= 22 || hour <= 6) {
                const nightOperations = data.filter(d => {
                    const h = d.操作时间.getHours();
                    return (h >= 22 || h <= 6) && 
                           d.操作时间.toDateString() === row.操作时间.toDateString();
                });
                
                if (nightOperations.length > 5) {
                    boost += 0.3;
                }
            }
            
            // 国际漫游突增检测
            if (row.业务类型 === '国际漫游') {
                const weekAgo = new Date(row.操作时间.getTime() - 7 * 24 * 60 * 60 * 1000);
                const recentRoaming = data.filter(d => 
                    d.业务类型 === '国际漫游' && 
                    d.操作时间 >= weekAgo && 
                    d.操作时间 <= row.操作时间
                );
                
                if (recentRoaming.length > 3) {
                    boost += 0.5;
                }
            }
            
            return boost;
        }

        function analyzeTimePatterns(data) {
            const hourlyCounts = {};
            for (let i = 0; i < 24; i++) {
                hourlyCounts[i] = 0;
            }
            
            data.forEach(row => {
                const hour = row.操作时间.getHours();
                hourlyCounts[hour]++;
            });
            
            return hourlyCounts;
        }

        function generateVisualizations(data, riskScores) {
            // 时间轴图
            const timelineData = data.map(row => ({
                time: row.操作时间,
                amount: row.费用金额,
                risk: riskScores[String(row.账单编号)] || 0,
                billId: row.账单编号
            })).sort((a, b) => a.time - b.time);

            const timelineTrace = {
                x: timelineData.map(d => d.time),
                y: timelineData.map(d => d.amount),
                mode: 'markers',
                type: 'scatter',
                marker: {
                    size: 8,
                    color: timelineData.map(d => d.risk),
                    colorscale: 'RdYlGn_r',
                    showscale: true,
                    colorbar: { title: "风险评分" }
                },
                text: timelineData.map(d => `账单: ${d.billId}<br>金额: ${d.amount}<br>风险: ${d.risk.toFixed(3)}`),
                hoverinfo: 'text'
            };

            const timelineLayout = {
                title: '费用金额时间轴 (颜色表示风险评分)',
                xaxis: { title: '时间' },
                yaxis: { title: '费用金额 (元)' },
                height: 400
            };

            Plotly.newPlot('timelineChart', [timelineTrace], timelineLayout);

            // 风险分布图
            const riskValues = Object.values(riskScores);
            const riskTrace = {
                x: riskValues,
                type: 'histogram',
                nbinsx: 20,
                marker: { color: 'lightblue', opacity: 0.7 }
            };

            const riskLayout = {
                title: '风险评分分布',
                xaxis: { title: '风险评分' },
                yaxis: { title: '频次' },
                shapes: [
                    { type: 'line', x0: 0.3, x1: 0.3, y0: 0, y1: 1, yref: 'paper', 
                      line: { dash: 'dash', color: 'orange' } },
                    { type: 'line', x0: 0.7, x1: 0.7, y0: 0, y1: 1, yref: 'paper', 
                      line: { dash: 'dash', color: 'red' } }
                ],
                annotations: [
                    { x: 0.3, y: 0.9, yref: 'paper', text: '低风险阈值', showarrow: false },
                    { x: 0.7, y: 0.9, yref: 'paper', text: '高风险阈值', showarrow: false }
                ]
            };

            Plotly.newPlot('riskDistributionChart', [riskTrace], riskLayout);

            // 操作员分析图
            const operatorStats = {};
            data.forEach(row => {
                const operatorId = row.操作员ID;
                const risk = riskScores[String(row.账单编号)] || 0;
                
                if (!operatorStats[operatorId]) {
                    operatorStats[operatorId] = { risks: [], count: 0 };
                }
                operatorStats[operatorId].risks.push(risk);
                operatorStats[operatorId].count++;
            });

            const operatorTrace = {
                x: Object.values(operatorStats).map(s => s.count),
                y: Object.values(operatorStats).map(s => s.risks.reduce((a, b) => a + b, 0) / s.risks.length),
                mode: 'markers',
                type: 'scatter',
                marker: {
                    size: Object.values(operatorStats).map(s => Math.max(10, s.count / 2)),
                    color: Object.values(operatorStats).map(s => s.risks.reduce((a, b) => a + b, 0) / s.risks.length),
                    colorscale: 'RdYlGn_r',
                    showscale: true,
                    colorbar: { title: "平均风险评分" }
                },
                text: Object.keys(operatorStats),
                hoverinfo: 'text+x+y'
            };

            const operatorLayout = {
                title: '操作员风险分析',
                xaxis: { title: '操作次数' },
                yaxis: { title: '平均风险评分' },
                height: 400
            };

            Plotly.newPlot('operatorChart', [operatorTrace], operatorLayout);
        }

        function showResults(data, riskScores) {
            const riskValues = Object.values(riskScores);
            const highRiskCount = riskValues.filter(v => v >= 0.7).length;
            const mediumRiskCount = riskValues.filter(v => v >= 0.3 && v < 0.7).length;
            const lowRiskCount = riskValues.filter(v => v < 0.3).length;
            const avgRisk = riskValues.reduce((a, b) => a + b, 0) / riskValues.length;

            const statsGrid = document.getElementById('statsGrid');
            statsGrid.innerHTML = `
                <div class="stat-card">
                    <div class="stat-number">${data.length}</div>
                    <div class="stat-label">总记录数</div>
                </div>
                <div class="stat-card">
                    <div class="stat-number">${highRiskCount}</div>
                    <div class="stat-label">高风险记录</div>
                </div>
                <div class="stat-card">
                    <div class="stat-number">${mediumRiskCount}</div>
                    <div class="stat-label">中风险记录</div>
                </div>
                <div class="stat-card">
                    <div class="stat-number">${avgRisk.toFixed(3)}</div>
                    <div class="stat-label">平均风险评分</div>
                </div>
            `;

            // 生成风险表格
            const highRiskRecords = Object.entries(riskScores)
                .filter(([_, score]) => score >= 0.7)
                .sort(([_, a], [__, b]) => b - a)
                .slice(0, 20);

            const tableHtml = `
                <table class="risk-table">
                    <thead>
                        <tr>
                            <th>排名</th>
                            <th>账单编号</th>
                            <th>风险评分</th>
                            <th>风险等级</th>
                        </tr>
                    </thead>
                    <tbody>
                        ${highRiskRecords.map(([billId, score], index) => {
                            const riskLevel = score >= 0.7 ? '高风险' : score >= 0.3 ? '中风险' : '低风险';
                            const rowClass = score >= 0.7 ? 'high-risk' : score >= 0.3 ? 'medium-risk' : 'low-risk';
                            return `
                                <tr class="${rowClass}">
                                    <td>${index + 1}</td>
                                    <td>${billId}</td>
                                    <td>${score.toFixed(3)}</td>
                                    <td>${riskLevel}</td>
                                </tr>
                            `;
                        }).join('')}
                    </tbody>
                </table>
            `;

            document.getElementById('riskTable').innerHTML = tableHtml;
            document.getElementById('resultsSection').style.display = 'block';
        }

        function exportReport() {
            if (!currentData || !riskScores) {
                showError('没有可导出的数据');
                return;
            }

            // 创建报告数据
            const reportData = currentData.map(row => ({
                ...row,
                风险评分: riskScores[String(row.账单编号)] || 0,
                风险等级: (() => {
                    const score = riskScores[String(row.账单编号)] || 0;
                    return score >= 0.7 ? '高风险' : score >= 0.3 ? '中风险' : '低风险';
                })()
            }));

            // 创建工作簿
            const wb = XLSX.utils.book_new();
            const ws = XLSX.utils.json_to_sheet(reportData);
            XLSX.utils.book_append_sheet(wb, ws, "异常检测结果");

            // 导出文件
            XLSX.writeFile(wb, `资费异常检测报告_${new Date().toISOString().split('T')[0]}.xlsx`);
            showSuccess('报告已导出');
        }

        function showLoading(show) {
            document.getElementById('loading').style.display = show ? 'block' : 'none';
        }

        function showError(message) {
            const errorDiv = document.createElement('div');
            errorDiv.className = 'error';
            errorDiv.textContent = message;
            document.querySelector('.upload-section').appendChild(errorDiv);
            setTimeout(() => errorDiv.remove(), 5000);
        }

        function showSuccess(message) {
            const successDiv = document.createElement('div');
            successDiv.className = 'success';
            successDiv.textContent = message;
            document.querySelector('.upload-section').appendChild(successDiv);
            setTimeout(() => successDiv.remove(), 5000);
        }

        function generateAndDetectSample() {
            // 生成100条模拟账单数据，低:中:高风险比例约7:2:1，并增加"高/中风险标记"字段
            const businessTypes = [
                ["开户", [100, 200]],
                ["销户", [50, 100]],
                ["套餐变更", [100, 500]],
                ["充值", [10, 1000]],
                ["流量包", [5, 200]],
                ["国际漫游", [100, 2000]]
            ];
            const operatorIds = ["OP001", "OP002", "OP003"];
            const branchIds = ["BR001", "BR002", "BR003"];
            const rows = [];
            const baseTime = new Date(2024, 0, 15, 8, 0, 0);
            const total = 100;
            const highCount = Math.round(total * 0.1);   // 10条高风险
            const mediumCount = Math.round(total * 0.2); // 20条中风险
            const lowCount = total - highCount - mediumCount; // 70条低风险
            let idx = 0;
            // 先生成高风险
            for (let i = 0; i < highCount; i++, idx++) {
                const [btype, [amin, amax]] = businessTypes[Math.floor(Math.random() * businessTypes.length)];
                let amount = +(Math.random() * 5000 + 3000).toFixed(2);
                let discount = +(Math.random() * amount * 0.2).toFixed(2);
                let realAmount = +(amount - discount).toFixed(2);
                const opId = operatorIds[Math.floor(Math.random() * operatorIds.length)];
                const brId = branchIds[Math.floor(Math.random() * branchIds.length)];
                const date = new Date(baseTime.getTime() + Math.floor(idx / 15) * 24 * 3600 * 1000);
                let hour = [0, 1, 2, 3, 4, 23][Math.floor(Math.random() * 6)];
                let minute = Math.floor(Math.random() * 60);
                const timeStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${hour.toString().padStart(2,'0')}:${minute.toString().padStart(2,'0')}:00`;
                rows.push({
                    "账单编号": `BILL${(idx+1).toString().padStart(3,'0')}`,
                    "营业厅编号": brId,
                    "账单日期": `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')}`,
                    "操作员ID": opId,
                    "业务类型": btype,
                    "费用金额": amount,
                    "优惠金额": discount,
                    "实收金额": realAmount,
                    "操作时间": timeStr,
                    "高风险标记": "是",
                    "中风险标记": "否"
                });
            }
            // 再生成中风险
            for (let i = 0; i < mediumCount; i++, idx++) {
                const [btype, [amin, amax]] = businessTypes[Math.floor(Math.random() * businessTypes.length)];
                let amount = +(Math.random() * (amax * 2 - amin) + amin).toFixed(2); // 适度偏高
                let discount = +(Math.random() * amount * 0.2).toFixed(2);
                let realAmount = +(amount - discount).toFixed(2);
                const opId = operatorIds[Math.floor(Math.random() * operatorIds.length)];
                const brId = branchIds[Math.floor(Math.random() * branchIds.length)];
                const date = new Date(baseTime.getTime() + Math.floor(idx / 15) * 24 * 3600 * 1000);
                let hour = [6, 7, 8, 21, 22][Math.floor(Math.random() * 5)]; // 营业边缘时段
                let minute = Math.floor(Math.random() * 60);
                const timeStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${hour.toString().padStart(2,'0')}:${minute.toString().padStart(2,'0')}:00`;
                rows.push({
                    "账单编号": `BILL${(idx+1).toString().padStart(3,'0')}`,
                    "营业厅编号": brId,
                    "账单日期": `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')}`,
                    "操作员ID": opId,
                    "业务类型": btype,
                    "费用金额": amount,
                    "优惠金额": discount,
                    "实收金额": realAmount,
                    "操作时间": timeStr,
                    "高风险标记": "否",
                    "中风险标记": "是"
                });
            }
            // 最后生成低风险
            for (let i = 0; i < lowCount; i++, idx++) {
                const [btype, [amin, amax]] = businessTypes[Math.floor(Math.random() * businessTypes.length)];
                let amount = +(Math.random() * (amax - amin) + amin).toFixed(2);
                let discount = +(Math.random() * amount * 0.2).toFixed(2);
                let realAmount = +(amount - discount).toFixed(2);
                const opId = operatorIds[Math.floor(Math.random() * operatorIds.length)];
                const brId = branchIds[Math.floor(Math.random() * branchIds.length)];
                const date = new Date(baseTime.getTime() + Math.floor(idx / 15) * 24 * 3600 * 1000);
                let hour = Math.floor(Math.random() * 12) + 8; // 正常营业时段
                let minute = Math.floor(Math.random() * 60);
                const timeStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${hour.toString().padStart(2,'0')}:${minute.toString().padStart(2,'0')}:00`;
                rows.push({
                    "账单编号": `BILL${(idx+1).toString().padStart(3,'0')}`,
                    "营业厅编号": brId,
                    "账单日期": `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')}`,
                    "操作员ID": opId,
                    "业务类型": btype,
                    "费用金额": amount,
                    "优惠金额": discount,
                    "实收金额": realAmount,
                    "操作时间": timeStr,
                    "高风险标记": "否",
                    "中风险标记": "否"
                });
            }
            currentData = rows;
            showSuccess('已生成100条模拟账单数据（低:中:高 ≈ 7:2:1）');
            document.getElementById('detectBtn').disabled = false;
            detectAnomalies();
        }
    </script>
</body>
</html> 